/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package ledger

import (
	reqContext "context"
	"net/http"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/verifier"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/transactor"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"

	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/multi"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/logging"
)

var logger = logging.NewLogger("ichainsdk/ichain")

type Ledger struct {
	chName string
}

func NewLedger(chName string) (*Ledger, error) {
	l := Ledger{
		chName: chName,
	}
	return &l, nil
}

func (c *Ledger) QueryInfo(reqCtx reqContext.Context, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*ichain.BlockchainInfoResponse, error) {
	logger.Debug("queryInfo - start")

	cir := createChainInfoInvokeRequest(c.chName)
	tprs, errs := queryContract(reqCtx, c.chName, cir, targets, verifier)

	responses := []*ichain.BlockchainInfoResponse{}
	for _, tpr := range tprs {
		r, err := createBlockchainInfo(tpr)
		if err != nil {
			errs = multi.Append(errs, errors.WithMessage(err, "From target: "+tpr.Endorser))
		} else {
			responses = append(responses, &ichain.BlockchainInfoResponse{Endorser: tpr.Endorser, Status: tpr.Status, BCI: r})
		}
	}
	return responses, errs
}

func createBlockchainInfo(tpr *ichain.TransactionProposalResponse) (*common.BlockChainInfo, error) {
	response := common.BlockChainInfo{}
	err := proto.Unmarshal(tpr.ProposalResponse.GetResponse().Payload, &response)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal of transaction proposal response failed")
	}
	return &response, nil
}

func (c *Ledger) QueryBlockByHash(reqCtx reqContext.Context, blockHash []byte, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*common.Block, error) {

	if len(blockHash) == 0 {
		return nil, errors.New("blockHash is required")
	}

	cir := createBlockByHashInvokeRequest(c.chName, blockHash)
	tprs, errs := queryContract(reqCtx, c.chName, cir, targets, verifier)

	responses, allErr := getConfigBlocks(tprs)
	errs = multi.Append(errs, allErr)

	return responses, errs
}

func (c *Ledger) QueryBlockByTxID(reqCtx reqContext.Context, txID ichain.TransactionID, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*common.Block, error) {

	if txID == "" {
		return nil, errors.New("txID is required")
	}

	cir := createBlockByTxIDInvokeRequest(c.chName, txID)
	tprs, errs := queryContract(reqCtx, c.chName, cir, targets, verifier)

	responses, errors := getConfigBlocks(tprs)
	errs = multi.Append(errs, errors)

	return responses, errs
}

func getConfigBlocks(tprs []*ichain.TransactionProposalResponse) ([]*common.Block, error) {
	responses := []*common.Block{}
	var errs error
	for _, tpr := range tprs {
		r, err := tpr.CreateCommonBlock()
		if err != nil {
			errs = multi.Append(errs, errors.WithMessage(err, "From target: "+tpr.Endorser))
		} else {
			responses = append(responses, r)
		}
	}
	return responses, errs
}

func (c *Ledger) QueryBlock(reqCtx reqContext.Context, blockNumber uint64, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*common.Block, error) {

	cir := createBlockByNumberInvokeRequest(c.chName, blockNumber)
	tprs, errs := queryContract(reqCtx, c.chName, cir, targets, verifier)

	responses, errors := getConfigBlocks(tprs)
	errs = multi.Append(errs, errors)
	return responses, errs
}

func (c *Ledger) QueryTransaction(reqCtx reqContext.Context, transactionID ichain.TransactionID, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*pb.ProcessedTransaction, error) {

	cir := createTransactionByIDInvokeRequest(c.chName, transactionID)
	tprs, errs := queryContract(reqCtx, c.chName, cir, targets, verifier)

	responses := []*pb.ProcessedTransaction{}
	for _, tpr := range tprs {
		r, err := createProcessedTransaction(tpr)
		if err != nil {
			errs = multi.Append(errs, errors.WithMessage(err, "From target: "+tpr.Endorser))
		} else {
			responses = append(responses, r)
		}
	}

	return responses, errs
}

func createProcessedTransaction(tpr *ichain.TransactionProposalResponse) (*pb.ProcessedTransaction, error) {
	response := pb.ProcessedTransaction{}
	err := proto.Unmarshal(tpr.ProposalResponse.GetResponse().Payload, &response)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal of transaction proposal response failed")
	}
	return &response, err
}

func (c *Ledger) QueryInstantiatedContracts(reqCtx reqContext.Context, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*pb.ContractQueryResponse, error) {
	cir := createContractInvokeRequest()
	tprs, errs := queryContract(reqCtx, c.chName, cir, targets, verifier)

	responses := []*pb.ContractQueryResponse{}
	for _, tpr := range tprs {
		r, err := createContractQueryResponse(tpr)
		if err != nil {
			errs = multi.Append(errs, errors.WithMessage(err, "From target: "+tpr.Endorser))
		} else {
			responses = append(responses, r)
		}
	}
	return responses, errs
}

func createContractQueryResponse(tpr *ichain.TransactionProposalResponse) (*pb.ContractQueryResponse, error) {
	response := pb.ContractQueryResponse{}
	err := proto.Unmarshal(tpr.ProposalResponse.GetResponse().Payload, &response)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal of transaction proposal response failed")
	}
	return &response, nil
}

func (c *Ledger) QueryConfigBlock(reqCtx reqContext.Context, targets []ichain.Inode, verifier verifier.ResponseVerifier) (*common.Block, error) {
	if len(targets) == 0 {
		return nil, errors.New("target(s) required")
	}

	cir := createConfigBlockInvokeRequest(c.chName)
	tprs, err := queryContract(reqCtx, c.chName, cir, targets, verifier)
	if err != nil && len(tprs) == 0 {
		return nil, errors.WithMessage(err, "queryContract failed")
	}

	matchErr := verifier.Match(tprs)
	if matchErr != nil {
		return nil, matchErr
	}

	return tprs[0].CreateCommonBlock()
}

func queryContract(reqCtx reqContext.Context, chainID string, request ichain.ContractInvokeRequest, targets []ichain.Inode, verifier verifier.ResponseVerifier) ([]*ichain.TransactionProposalResponse, error) {
	tx := transactor.NewTransactor(reqCtx, chainID)

	tp, err := tx.CreateTxProposal(request)
	if err != nil {
		return nil, errors.WithMessage(err, "NewProposal failed")
	}
	tprs, errs := tx.SendTxProposal(tp, targets)

	return filterResponses(tprs, errs, verifier)
}

func filterResponses(responses []*ichain.TransactionProposalResponse, errs error, verifier verifier.ResponseVerifier) ([]*ichain.TransactionProposalResponse, error) {
	filteredResponses := responses[:0]
	for _, response := range responses {
		if response.Status == http.StatusOK {
			if verifier != nil {
				if err := verifier.Verify(response); err != nil {
					errs = multi.Append(errs, errors.Errorf("failed to verify response from %s: %s", response.Endorser, err))
					continue
				}
			}
			filteredResponses = append(filteredResponses, response)
		} else {
			errs = multi.Append(errs, errors.Errorf("bad status from %s (%d)", response.Endorser, response.Status))
		}
	}

	return filteredResponses, errs
}
